﻿// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. 
// See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Globalization;
using System.Fabric;
using System.Text;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Threading.Actors.Runtime;
using System.Threading.Actors;
using System.IO;
using System.Reflection;
using System.Diagnostics;
using System.Runtime.Serialization;
using System.Diagnostics.Contracts;

namespace ActorDirectClient
{
    [Serializable]
    public class SomeArbitraryClass
    {
        public int PropertyA { get; set; }
        public double PropertyB { get; set; }
        public string PropertyC { get; set; }

        public override string ToString()
        {
            return String.Format("[{0},{1},{2}]", PropertyA, PropertyB, PropertyC);
        }
    }

    public class ActorCallbackListener : IPublicationCallbackContract
    {
        public void OnNewEvent(ActorEvent actorEvent)
        {
            Console.WriteLine("Actor published event type {0}, payload {1}", actorEvent.EventType, actorEvent.EventPayload);
        }

        public void OnCompleting()
        {
            Console.WriteLine("Actor completing");
        }
    }

    public class ActorEventObserver : IObserver<ActorEvent>
    {
        private string m_eventType;

        public ActorEventObserver(string eventType)
        {
            m_eventType = eventType;
        }

        public void OnCompleted()
        {
            Console.WriteLine("Observer({0}) got completion event", m_eventType);
        }

        public void OnError(Exception error)
        {
            Console.WriteLine("Observer({0}) got error: {1}", m_eventType, error.Message);
        }

        public void OnNext(ActorEvent value)
        {
            Console.WriteLine("Observer({0}) saw value {1}", m_eventType, value);
        }
    }


    public static class ActorDirectClient
    {
        private static readonly Dictionary<string, IDisposable> m_observerDisposers = new Dictionary<string, IDisposable>();

        [ActorMethod]
        public static object SayHello(IActorState state, object[] parameters)
        {
            return "Hola!";
        }

        [ActorMethod]
        public static object Add2Ints(IActorState state, object[] parameters)
        {
            if (parameters == null) return "ERROR: null parameter list";
            if (parameters.Length != 2) return "ERROR: Expected 2 parameters, got " + parameters.Length;
            var i1 = Int32.Parse((string)parameters[0]);
            var i2 = Int32.Parse((string)parameters[1]);
            return i1 + i2;
        }

        [ActorMethod]
        public static object AddToCounter(IActorState state, object[] parameters)
        {
            Contract.Requires(state != null);

            if (parameters == null) return "ERROR: null parameter list";
            if (parameters.Length != 1) return "ERROR: Expected 1 parameter, got " + parameters.Length;
            var amountToAdd = Int32.Parse((string)parameters[0]);
            
            int count = 0;
            object temp;
            if (state.TryGet("_count", out temp)) count = (int)temp;

            count += amountToAdd;
            state.Set("_count", count);
            return count;
        }

        public static void Main(string[] args)
        {
            Console.WriteLine();
            Console.WriteLine("Welcome to the Direct Actor Client");

            String[] hostEndpoints = args;
            if (hostEndpoints.Length == 0)
                hostEndpoints = new String[] { "localhost:19000" };  // by default

            FabricClient client = new FabricClient(hostEndpoints);
            Uri subName = new Uri("Fabric:/actor");
            int count = 0;
            Dictionary<int, Uri> choices = new Dictionary<int, Uri>();
            Uri chosenUri = null;
            try
            {
                NameEnumerationResult nameResult = null;
                do
                {
                    // See what actor URI's exist.
                    Task<NameEnumerationResult> enumTask = client.PropertyManager.EnumerateSubNamesAsync(subName, nameResult, true);
                    enumTask.Wait();
                    nameResult = enumTask.Result;
                    // Each batch has two flags: HasMore and Consistent. 
                    // Consistent indicates whether the relevant naming data in the cluster is currently being updated 
                    // HasMore indicates whether there are othe batches that remain.
                    // If there are other batches, the user needs to recall the EnumerateSubNamesAsync and give the latest batch as the previousResult.
                    // PreviousResult makes sure that the subsequent batch is returned.                    
                    foreach (Uri name in nameResult)
                    {
                        choices.Add(count, name);
                        Console.WriteLine("{0}: \t{1}", count, name);
                        count++;
                    }
                } while (nameResult.HasMoreData);

                Console.Write("To which actor would you like to connect? ");
                string sval = Console.ReadLine();
                Contract.Assume(sval != null, "Console.ReadLine returned null.  End of file?");
                int actorNum = Int32.Parse(sval);
                chosenUri = choices[actorNum];
            }
            catch (Exception e)
            {
                Console.WriteLine("Exception when enumerating actors: {0}", e);
                Console.Write("Press <Enter> to quit");
                Console.ReadLine();
            }


            //FabricClient tmp = new FabricClient(args);
            //FabricBCL.Utility.EnumerateAndPrintSubNames(tmp, new Uri("fabric:/samples"));

            Console.WriteLine("Would you like to use a Gateway Service to connect to the Windows Fabric Service?");
            Console.WriteLine("Windows Gateway Service enables users to use Windows Fabric Services running in Azure from outside of Azure");
            Console.WriteLine("  [y]es");
            Console.WriteLine("  [n]o");

            bool useGateway = Console.ReadLine().Equals("y", StringComparison.CurrentCultureIgnoreCase);

            var fabricUri = new Uri(string.Format("net.tcp://{0}", hostEndpoints[0]));
            //var fabricUri = new Uri(args[0]);
            var actorClient = new FabricActorClient(fabricUri, chosenUri, useGateway);

            StartActorClient(actorClient);

            Console.WriteLine("Bye!");
        }



        // Starts the Actor Client for operations of the service.
        private static bool StartActorClient(ActorClient actorClient, bool usingGateway = false, string partitionName = null)
        {
            while (true)
            {
                Console.Write("Enter method and parameters: ");

                string selection = Console.ReadLine().Trim();

                // Intercept a few special commands
                if (selection == "q" || selection == "quit")
                {
                    Console.WriteLine("Quitting!");
                    return false;
                }
                else if (String.IsNullOrWhiteSpace(selection))
                {
                    continue;
                }
                else if (selection.StartsWith("AddAssembly"))
                {
                    // TODO: Support arbitrary assemblies.   For now, just shipping our assembly.
                    var myAssembly = Assembly.GetExecutingAssembly();
                    var assemblyPath = myAssembly.Location;
                    //var assemblyName = myAssembly.ManifestModule.Name;
                    var assemblyName = myAssembly.FullName;

                    byte[] buffer = File.ReadAllBytes(assemblyPath);

                    bool result = actorClient.AddAssembly(assemblyName, buffer);
                    Console.WriteLine("Result of AddAssembly: {0}", result);
                }
                else if (selection.StartsWith("SUBSCRIBE"))
                {
                    string[] parts = selection.Split(' ');
                    for (int i = 1; i < parts.Length; i++)
                    {
                        var eventType = parts[i];
                        var observer = new ActorEventObserver(eventType);
                        var disposer = actorClient.Subscribe(eventType, observer);
                        m_observerDisposers[eventType] = disposer;
                    }
                }
                else if (selection.StartsWith("UNSUBSCRIBE"))
                {
                    string[] parts = selection.Split(' ');
                    for (int i = 1; i < parts.Length; i++)
                    {
                        var eventType = parts[i];
                        IDisposable disposer;
                        if (m_observerDisposers.TryGetValue(eventType, out disposer))
                        {
                            m_observerDisposers.Remove(eventType);
                            disposer.Dispose();
                        }
                    }
                }
                else if (selection.StartsWith("TIME ", StringComparison.InvariantCultureIgnoreCase))
                {
                    string[] methodAndParameters = selection.Split(' ');
                    int iterations = Int32.Parse(methodAndParameters[1]);
                    string methodName = methodAndParameters[2];
                    object[] parameters = new object[methodAndParameters.Length - 3];
                    for (int i = 3; i < methodAndParameters.Length; i++) parameters[i - 3] = methodAndParameters[i];
                    var sw = Stopwatch.StartNew();
                    int successfulOps = 0;
                    for (int i = 0; i < iterations; i++)
                    {
                        object result;
                        Exception exception;
                        if (actorClient.CallMethodNoThrow(methodName, parameters, out result, out exception)) successfulOps++;
                        //object result = actorClient.EvalFuncRequest(new EvalFunctionRequest((state, args) =>
                        //{
                        //    return state.CallMethod(args);
                        //}, parameters));
                    }
                    sw.Stop();
                    Console.WriteLine("Time for {0} iterations ({3} successful) of \"{1}\": {2} ms", iterations, methodAndParameters.Skip(2).Aggregate("", (acc, val) => acc + " " + val), sw.ElapsedMilliseconds, successfulOps);
                }
                else if (selection.StartsWith("ITIME ", StringComparison.InvariantCultureIgnoreCase))
                {
                    // like time but parameters may have {0} which is replaced with iteration value
                    string[] methodAndParameters = selection.Split(' ');
                    int iterations = Int32.Parse(methodAndParameters[1]);
                    string methodName = methodAndParameters[2];
                    var sw = Stopwatch.StartNew();
                    int successfulOps = 0;
                    for (int i = 0; i < iterations; i++)
                    {
                        object[] parameters = new object[methodAndParameters.Length - 3];
                        for (int j = 3; j < methodAndParameters.Length; j++)
                            parameters[j - 3] = String.Format(methodAndParameters[j], i);
                        object result;
                        Exception exception;
                        if (actorClient.CallMethodNoThrow(methodName, parameters, out result, out exception)) successfulOps++;
                    }
                    sw.Stop();
                    Console.WriteLine("Time for {0} iterations ({3} successful) of \"{1}\": {2} ms", iterations, methodAndParameters.Skip(2).Aggregate("", (acc, val) => acc + " " + val), sw.ElapsedMilliseconds, successfulOps);
                }
                else
                {
                    string[] methodAndParameters = selection.Split(' ');
                    string methodName = methodAndParameters[0];
                    object[] parameters = new object[methodAndParameters.Length - 1];
                    for (int i = 1; i < methodAndParameters.Length; i++)
                    {
                        var arg = methodAndParameters[i];
                        if (arg.StartsWith("CUSTOM"))
                        {
                            parameters[i - 1] = new SomeArbitraryClass { PropertyA = 42, PropertyB = 42.0, PropertyC = "Forty-Two" };
                        }
                        else parameters[i - 1] = methodAndParameters[i];
                    }
                    object result;
                    Exception exception;
                    actorClient.CallMethodNoThrow(methodName, parameters, out result, out exception);

                    //object result = actorClient.EvalFuncRequest(new EvalFunctionRequest((state, parameters) =>
                    //{
                    //    return state.CallMethod(parameters);
                    //}, methodAndParameters));

                    if (result is List<string>)
                    {
                        var lresult = result as List<string>;
                        result = lresult.Aggregate("", (acc, element) => acc + " " + element, acc => acc);
                    }
                    else if (result is List<object>)
                    {
                        var oresult = result as List<object>;
                        result = oresult.Aggregate("", (acc, element) => acc + " " + element.ToString(), acc => acc);
                    }
                    Console.WriteLine("Result: {0}", result); // use null or static instead?
                    if (exception != null) Console.WriteLine("(Exception: {0})", exception);
                }
            }
        }
    }

}
